/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.fima.glowpadview;

import java.util.ArrayList;

import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Vibrator;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityManager;

/**
 * This is a copy of com.android.internal.widget.multiwaveview.GlowPadView with
 * minor changes to remove dependencies on private api's.
 * 
 * Contains changes up to If296b60af2421bfa1a9a082e608ba77b2392a218
 * 
 * A re-usable widget containing a center, outer ring and wave animation.
 */
public class GlowPadView extends View {
	private static final String TAG = "GlowPadView";
	private static final boolean DEBUG = false;

	// Wave state machine
	private static final int STATE_IDLE = 0;
	private static final int STATE_START = 1;
	private static final int STATE_FIRST_TOUCH = 2;
	private static final int STATE_TRACKING = 3;
	private static final int STATE_SNAP = 4;
	private static final int STATE_FINISH = 5;

	// Animation properties.
	private static final float SNAP_MARGIN_DEFAULT = 20.0f; // distance to ring
															// before we snap to
															// it

	public interface OnTriggerListener {
		int NO_HANDLE = 0;
		int CENTER_HANDLE = 1;

		public void onGrabbed(View v, int handle);

		public void onReleased(View v, int handle);

		public void onTrigger(View v, int target);

		public void onGrabbedStateChange(View v, int handle);

		public void onFinishFinalAnimation();
	}

	// Tuneable parameters for animation
	private static final int WAVE_ANIMATION_DURATION = 1350;
	private static final int RETURN_TO_HOME_DELAY = 1200;
	private static final int RETURN_TO_HOME_DURATION = 200;
	private static final int HIDE_ANIMATION_DELAY = 200;
	private static final int HIDE_ANIMATION_DURATION = 200;
	private static final int SHOW_ANIMATION_DURATION = 200;
	private static final int SHOW_ANIMATION_DELAY = 50;
	private static final int INITIAL_SHOW_HANDLE_DURATION = 200;
	private static final int REVEAL_GLOW_DELAY = 0;
	private static final int REVEAL_GLOW_DURATION = 0;

	private static final float TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.3f;
	private static final float TARGET_SCALE_EXPANDED = 1.0f;
	private static final float TARGET_SCALE_COLLAPSED = 0.8f;
	private static final float RING_SCALE_EXPANDED = 1.0f;
	private static final float RING_SCALE_COLLAPSED = 0.5f;

	private ArrayList<TargetDrawable> mTargetDrawables = new ArrayList<TargetDrawable>();
	private AnimationBundle mWaveAnimations = new AnimationBundle();
	private AnimationBundle mTargetAnimations = new AnimationBundle();
	private AnimationBundle mGlowAnimations = new AnimationBundle();
	private ArrayList<String> mTargetDescriptions;
	private ArrayList<String> mDirectionDescriptions;
	private OnTriggerListener mOnTriggerListener;
	private TargetDrawable mHandleDrawable;
	private TargetDrawable mOuterRing;
	private Vibrator mVibrator;

	private int mFeedbackCount = 3;
	private int mVibrationDuration = 0;
	private int mGrabbedState;
	private int mActiveTarget = -1;
	private float mGlowRadius;
	private float mWaveCenterX;
	private float mWaveCenterY;
	private int mMaxTargetHeight;
	private int mMaxTargetWidth;

	private float mOuterRadius = 0.0f;
	private float mSnapMargin = 0.0f;
	private boolean mDragging;
	private int mNewTargetResources;

	private class AnimationBundle extends ArrayList<Tweener> {
		private static final long serialVersionUID = 0xA84D78726F127468L;
		private boolean mSuspended;

		public void start() {
			if (mSuspended)
				return; // ignore attempts to start animations
			final int count = size();
			for (int i = 0; i < count; i++) {
				Tweener anim = get(i);
				anim.animator.start();
			}
		}

		public void cancel() {
			final int count = size();
			for (int i = 0; i < count; i++) {
				Tweener anim = get(i);
				anim.animator.cancel();
			}
			clear();
		}

		public void stop() {
			final int count = size();
			for (int i = 0; i < count; i++) {
				Tweener anim = get(i);
				anim.animator.end();
			}
			clear();
		}

		public void setSuspended(boolean suspend) {
			mSuspended = suspend;
		}
	};

	private AnimatorListener mResetListener = new AnimatorListenerAdapter() {
		public void onAnimationEnd(Animator animator) {
			switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY);
			dispatchOnFinishFinalAnimation();
		}
	};

	private AnimatorListener mResetListenerWithPing = new AnimatorListenerAdapter() {
		public void onAnimationEnd(Animator animator) {
			ping();
			switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY);
			dispatchOnFinishFinalAnimation();
		}
	};

	private AnimatorUpdateListener mUpdateListener = new AnimatorUpdateListener() {
		public void onAnimationUpdate(ValueAnimator animation) {
			invalidate();
		}
	};

	private boolean mAnimatingTargets;
	private AnimatorListener mTargetUpdateListener = new AnimatorListenerAdapter() {
		public void onAnimationEnd(Animator animator) {
			if (mNewTargetResources != 0) {
				internalSetTargetResources(mNewTargetResources);
				mNewTargetResources = 0;
				hideTargets(false, false);
			}
			mAnimatingTargets = false;
		}
	};
	private int mTargetResourceId;
	private int mTargetDescriptionsResourceId;
	private int mDirectionDescriptionsResourceId;
	private boolean mAlwaysTrackFinger;
	private int mHorizontalInset;
	private int mVerticalInset;
	private int mGravity = Gravity.TOP;
	private boolean mInitialLayout = true;
	private Tweener mBackgroundAnimator;
	private PointCloud mPointCloud;
	private float mInnerRadius;
	private int mPointerId;
	private boolean mShowTargetsOnIdle;

	public GlowPadView(Context context) {
		this(context, null);
	}

	public GlowPadView(Context context, AttributeSet attrs) {
		super(context, attrs);
		Resources res = context.getResources();

		TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GlowPadView);
		mInnerRadius = a.getDimension(R.styleable.GlowPadView_innerRadius, mInnerRadius);
		mOuterRadius = a.getDimension(R.styleable.GlowPadView_outerRadius, mOuterRadius);
		mSnapMargin = a.getDimension(R.styleable.GlowPadView_snapMargin, mSnapMargin);
		mVibrationDuration = a.getInt(R.styleable.GlowPadView_vibrationDuration, mVibrationDuration);
		mFeedbackCount = a.getInt(R.styleable.GlowPadView_feedbackCount, mFeedbackCount);
		TypedValue handle = a.peekValue(R.styleable.GlowPadView_handleDrawable);
		mHandleDrawable = new TargetDrawable(res, handle != null ? handle.resourceId : 0, 2);
		mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE);
		mOuterRing = new TargetDrawable(res, getResourceId(a, R.styleable.GlowPadView_outerRingDrawable), 1);

		mAlwaysTrackFinger = a.getBoolean(R.styleable.GlowPadView_alwaysTrackFinger, false);

		int pointId = getResourceId(a, R.styleable.GlowPadView_pointDrawable);
		Drawable pointDrawable = pointId != 0 ? res.getDrawable(pointId) : null;
		mGlowRadius = a.getDimension(R.styleable.GlowPadView_glowRadius, 0.0f);

		TypedValue outValue = new TypedValue();

		// Read array of target drawables
		if (a.getValue(R.styleable.GlowPadView_targetDrawables, outValue)) {
			internalSetTargetResources(outValue.resourceId);
		}
		if (mTargetDrawables == null || mTargetDrawables.size() == 0) {
			throw new IllegalStateException("Must specify at least one target drawable");
		}

		// Read array of target descriptions
		if (a.getValue(R.styleable.GlowPadView_targetDescriptions, outValue)) {
			final int resourceId = outValue.resourceId;
			if (resourceId == 0) {
				throw new IllegalStateException("Must specify target descriptions");
			}
			setTargetDescriptionsResourceId(resourceId);
		}

		// Read array of direction descriptions
		if (a.getValue(R.styleable.GlowPadView_directionDescriptions, outValue)) {
			final int resourceId = outValue.resourceId;
			if (resourceId == 0) {
				throw new IllegalStateException("Must specify direction descriptions");
			}
			setDirectionDescriptionsResourceId(resourceId);
		}

		a.recycle();

		// Use gravity attribute from LinearLayout
		// a = context.obtainStyledAttributes(attrs, R.styleable.LinearLayout);
		mGravity = a.getInt(R.styleable.GlowPadView_android_gravity, Gravity.TOP);
		a.recycle();

		setVibrateEnabled(mVibrationDuration > 0);

		assignDefaultsIfNeeded();

		mPointCloud = new PointCloud(pointDrawable);
		mPointCloud.makePointCloud(mInnerRadius, mOuterRadius);
		mPointCloud.glowManager.setRadius(mGlowRadius);

		
	}

	private int getResourceId(TypedArray a, int id) {
		TypedValue tv = a.peekValue(id);
		return tv == null ? 0 : tv.resourceId;
	}

	private void dump() {
		Log.v(TAG, "Outer Radius = " + mOuterRadius);
		Log.v(TAG, "SnapMargin = " + mSnapMargin);
		Log.v(TAG, "FeedbackCount = " + mFeedbackCount);
		Log.v(TAG, "VibrationDuration = " + mVibrationDuration);
		Log.v(TAG, "GlowRadius = " + mGlowRadius);
		Log.v(TAG, "WaveCenterX = " + mWaveCenterX);
		Log.v(TAG, "WaveCenterY = " + mWaveCenterY);
	}

	public void suspendAnimations() {
		mWaveAnimations.setSuspended(true);
		mTargetAnimations.setSuspended(true);
		mGlowAnimations.setSuspended(true);
	}

	public void resumeAnimations() {
		mWaveAnimations.setSuspended(false);
		mTargetAnimations.setSuspended(false);
		mGlowAnimations.setSuspended(false);
		mWaveAnimations.start();
		mTargetAnimations.start();
		mGlowAnimations.start();
	}

	@Override
	protected int getSuggestedMinimumWidth() {
		// View should be large enough to contain the background + handle and
		// target drawable on either edge.
		return (int) (Math.max(mOuterRing.getWidth(), 2 * mOuterRadius) + mMaxTargetWidth);
	}

	@Override
	protected int getSuggestedMinimumHeight() {
		// View should be large enough to contain the unlock ring + target and
		// target drawable on either edge
		return (int) (Math.max(mOuterRing.getHeight(), 2 * mOuterRadius) + mMaxTargetHeight);
	}

	private int resolveMeasured(int measureSpec, int desired) {
		int result = 0;
		int specSize = MeasureSpec.getSize(measureSpec);
		switch (MeasureSpec.getMode(measureSpec)) {
		case MeasureSpec.UNSPECIFIED:
			result = desired;
			break;
		case MeasureSpec.AT_MOST:
			result = Math.min(specSize, desired);
			break;
		case MeasureSpec.EXACTLY:
		default:
			result = specSize;
		}
		return result;
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		final int minimumWidth = getSuggestedMinimumWidth();
		final int minimumHeight = getSuggestedMinimumHeight();
		int computedWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
		int computedHeight = resolveMeasured(heightMeasureSpec, minimumHeight);
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
			computeInsets((computedWidth - minimumWidth), (computedHeight - minimumHeight));
		setMeasuredDimension(computedWidth, computedHeight);
	}

	private void switchToState(int state, float x, float y) {
		switch (state) {
		case STATE_IDLE:
			deactivateTargets();
			hideGlow(0, 0, 0.0f, null);
			startBackgroundAnimation(0, 0.0f);
			mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE);
			mHandleDrawable.setAlpha(1.0f);
			if (mShowTargetsOnIdle) {
				showTargets(true);
			}
			break;

		case STATE_START:
			startBackgroundAnimation(0, 0.0f);
			if (mShowTargetsOnIdle) {
				showTargets(false);
			}
			break;

		case STATE_FIRST_TOUCH:
			mHandleDrawable.setAlpha(0.0f);
			deactivateTargets();
			if (mShowTargetsOnIdle) {
				showTargets(false);
			} else {
				showTargets(true);
			}

			startBackgroundAnimation(INITIAL_SHOW_HANDLE_DURATION, 1.0f);
			setGrabbedState(OnTriggerListener.CENTER_HANDLE);

			final AccessibilityManager accessibilityManager = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
			if (accessibilityManager.isEnabled()) {
				announceTargets();
			}
			break;

		case STATE_TRACKING:
			mHandleDrawable.setAlpha(0.0f);
			showGlow(REVEAL_GLOW_DURATION, REVEAL_GLOW_DELAY, 1.0f, null);
			break;

		case STATE_SNAP:
			// TODO: Add transition states (see
			// list_selector_background_transition.xml)
			mHandleDrawable.setAlpha(0.0f);
			showGlow(REVEAL_GLOW_DURATION, REVEAL_GLOW_DELAY, 0.0f, null);
			break;

		case STATE_FINISH:
			doFinish();
			break;
		}
	}

	private void showGlow(int duration, int delay, float finalAlpha, AnimatorListener finishListener) {
		mGlowAnimations.cancel();
		mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration, "ease", Ease.Cubic.easeIn, "delay", delay, "alpha", finalAlpha, "onUpdate",
				mUpdateListener, "onComplete", finishListener));
		mGlowAnimations.start();
	}

	private void hideGlow(int duration, int delay, float finalAlpha, AnimatorListener finishListener) {
		mGlowAnimations.cancel();
		mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration, "ease", Ease.Quart.easeOut, "delay", delay, "alpha", finalAlpha, "x", 0.0f, "y",
				0.0f, "onUpdate", mUpdateListener, "onComplete", finishListener));
		mGlowAnimations.start();
	}

	private void deactivateTargets() {
		final int count = mTargetDrawables.size();
		for (int i = 0; i < count; i++) {
			TargetDrawable target = mTargetDrawables.get(i);
			target.setState(TargetDrawable.STATE_INACTIVE);
		}
		mActiveTarget = -1;
	}

	/**
	 * Dispatches a trigger event to listener. Ignored if a listener is not set.
	 * 
	 * @param whichTarget
	 *            the target that was triggered.
	 */
	private void dispatchTriggerEvent(int whichTarget) {
		vibrate();
		if (mOnTriggerListener != null) {
			mOnTriggerListener.onTrigger(this, whichTarget);
		}
	}

	private void dispatchOnFinishFinalAnimation() {
		if (mOnTriggerListener != null) {
			mOnTriggerListener.onFinishFinalAnimation();
		}
	}

	private void doFinish() {
		final int activeTarget = mActiveTarget;
		final boolean targetHit = activeTarget != -1;

		if (targetHit) {
			if (DEBUG)
				Log.v(TAG, "Finish with target hit = " + targetHit);

			highlightSelected(activeTarget);

			// Inform listener of any active targets. Typically only one will be
			// active.
			hideGlow(RETURN_TO_HOME_DURATION, RETURN_TO_HOME_DELAY, 0.0f, mResetListener);
			dispatchTriggerEvent(activeTarget);
			if (!mAlwaysTrackFinger) {
				// Force ring and targets to finish animation to final expanded
				// state
				mTargetAnimations.stop();
			}
		} else {
			// Animate handle back to the center based on current state.
			hideGlow(HIDE_ANIMATION_DURATION, 0, 0.0f, mResetListenerWithPing);
			if (!mShowTargetsOnIdle)
				hideTargets(true, false);
		}

		setGrabbedState(OnTriggerListener.NO_HANDLE);
	}

	private void highlightSelected(int activeTarget) {
		// Highlight the given target and fade others
		mTargetDrawables.get(activeTarget).setState(TargetDrawable.STATE_ACTIVE);
		hideUnselected(activeTarget);
	}

	private void hideUnselected(int active) {
		for (int i = 0; i < mTargetDrawables.size(); i++) {
			if (i != active) {
				mTargetDrawables.get(i).setAlpha(0.0f);
			}
		}
	}

	private void hideTargets(boolean animate, boolean expanded) {
		mTargetAnimations.cancel();
		// Note: these animations should complete at the same time so that we
		// can swap out
		// the target assets asynchronously from the setTargetResources() call.
		mAnimatingTargets = animate;
		final int duration = animate ? HIDE_ANIMATION_DURATION : 0;
		final int delay = animate ? HIDE_ANIMATION_DELAY : 0;

		final float targetScale = expanded ? TARGET_SCALE_EXPANDED : TARGET_SCALE_COLLAPSED;
		final int length = mTargetDrawables.size();

		final TimeInterpolator interpolator = Ease.Cubic.easeOut;
		for (int i = 0; i < length; i++) {
			TargetDrawable target = mTargetDrawables.get(i);
			target.setState(TargetDrawable.STATE_INACTIVE);
			mTargetAnimations.add(Tweener.to(target, duration, "ease", interpolator, "alpha", 0.0f, "scaleX", targetScale, "scaleY", targetScale, "delay",
					delay, "onUpdate", mUpdateListener));
		}

		final float ringScaleTarget = expanded ? RING_SCALE_EXPANDED : RING_SCALE_COLLAPSED;
		mTargetAnimations.add(Tweener.to(mOuterRing, duration, "ease", interpolator, "alpha", 0.0f, "scaleX", ringScaleTarget, "scaleY", ringScaleTarget,
				"delay", delay, "onUpdate", mUpdateListener, "onComplete", mTargetUpdateListener));

		mTargetAnimations.start();
	}

	private void showTargets(boolean animate) {
		mTargetAnimations.stop();
		mAnimatingTargets = animate;
		final int delay = animate ? SHOW_ANIMATION_DELAY : 0;
		final int duration = animate ? SHOW_ANIMATION_DURATION : 0;
		final int length = mTargetDrawables.size();
		for (int i = 0; i < length; i++) {
			TargetDrawable target = mTargetDrawables.get(i);
			target.setState(TargetDrawable.STATE_INACTIVE);
			mTargetAnimations.add(Tweener.to(target, duration, "ease", Ease.Cubic.easeOut, "alpha", 1.0f, "scaleX", 1.0f, "scaleY", 1.0f, "delay", delay,
					"onUpdate", mUpdateListener));
		}
		mTargetAnimations.add(Tweener.to(mOuterRing, duration, "ease", Ease.Cubic.easeOut, "alpha", 1.0f, "scaleX", 1.0f, "scaleY", 1.0f, "delay", delay,
				"onUpdate", mUpdateListener, "onComplete", mTargetUpdateListener));

		mTargetAnimations.start();
	}

	private void vibrate() {
		if (mVibrator != null) {
			mVibrator.vibrate(mVibrationDuration);
		}
	}

	private ArrayList<TargetDrawable> loadDrawableArray(int resourceId) {
		Resources res = getContext().getResources();
		TypedArray array = res.obtainTypedArray(resourceId);
		final int count = array.length();
		ArrayList<TargetDrawable> drawables = new ArrayList<TargetDrawable>(count);
		for (int i = 0; i < count; i++) {
			TypedValue value = array.peekValue(i);
			TargetDrawable target = new TargetDrawable(res, value != null ? value.resourceId : 0, 3);
			drawables.add(target);
		}
		array.recycle();
		return drawables;
	}

	private void internalSetTargetResources(int resourceId) {
		final ArrayList<TargetDrawable> targets = loadDrawableArray(resourceId);
		mTargetDrawables = targets;
		mTargetResourceId = resourceId;

		int maxWidth = mHandleDrawable.getWidth();
		int maxHeight = mHandleDrawable.getHeight();
		final int count = targets.size();
		for (int i = 0; i < count; i++) {
			TargetDrawable target = targets.get(i);
			maxWidth = Math.max(maxWidth, target.getWidth());
			maxHeight = Math.max(maxHeight, target.getHeight());
		}
		if (mMaxTargetWidth != maxWidth || mMaxTargetHeight != maxHeight) {
			mMaxTargetWidth = maxWidth;
			mMaxTargetHeight = maxHeight;
			requestLayout(); // required to resize layout and call
								// updateTargetPositions()
		} else {
			updateTargetPositions(mWaveCenterX, mWaveCenterY);
			updatePointCloudPosition(mWaveCenterX, mWaveCenterY);
		}
	}

	/**
	 * Loads an array of drawables from the given resourceId.
	 * 
	 * @param resourceId
	 */
	public void setTargetResources(int resourceId) {
		if (mAnimatingTargets) {
			// postpone this change until we return to the initial state
			mNewTargetResources = resourceId;
		} else {
			internalSetTargetResources(resourceId);
		}
	}

	public int getTargetResourceId() {
		return mTargetResourceId;
	}

	/**
	 * Sets the resource id specifying the target descriptions for
	 * accessibility.
	 * 
	 * @param resourceId
	 *            The resource id.
	 */
	public void setTargetDescriptionsResourceId(int resourceId) {
		mTargetDescriptionsResourceId = resourceId;
		if (mTargetDescriptions != null) {
			mTargetDescriptions.clear();
		}
	}

	/**
	 * Gets the resource id specifying the target descriptions for
	 * accessibility.
	 * 
	 * @return The resource id.
	 */
	public int getTargetDescriptionsResourceId() {
		return mTargetDescriptionsResourceId;
	}

	/**
	 * Sets the resource id specifying the target direction descriptions for
	 * accessibility.
	 * 
	 * @param resourceId
	 *            The resource id.
	 */
	public void setDirectionDescriptionsResourceId(int resourceId) {
		mDirectionDescriptionsResourceId = resourceId;
		if (mDirectionDescriptions != null) {
			mDirectionDescriptions.clear();
		}
	}

	/**
	 * Gets the resource id specifying the target direction descriptions.
	 * 
	 * @return The resource id.
	 */
	public int getDirectionDescriptionsResourceId() {
		return mDirectionDescriptionsResourceId;
	}

	/**
	 * Enable or disable vibrate on touch.
	 * 
	 * @param enabled
	 */
	public void setVibrateEnabled(boolean enabled) {
		if (enabled && mVibrator == null) {
			mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
		} else {
			mVibrator = null;
		}
	}

	/**
	 * Starts wave animation.
	 * 
	 */
	public void ping() {
		if (mFeedbackCount > 0) {
			boolean doWaveAnimation = true;
			final AnimationBundle waveAnimations = mWaveAnimations;

			// Don't do a wave if there's already one in progress
			if (waveAnimations.size() > 0 && waveAnimations.get(0).animator.isRunning()) {
				long t = waveAnimations.get(0).animator.getCurrentPlayTime();
				if (t < WAVE_ANIMATION_DURATION / 2) {
					doWaveAnimation = false;
				}
			}

			if (doWaveAnimation) {
				startWaveAnimation();
			}
		}
	}

	private void stopAndHideWaveAnimation() {
		mWaveAnimations.cancel();
		mPointCloud.waveManager.setAlpha(0.0f);
	}

	private void startWaveAnimation() {
		mWaveAnimations.cancel();
		mPointCloud.waveManager.setAlpha(1.0f);
		mPointCloud.waveManager.setRadius(mHandleDrawable.getWidth() / 2.0f);
		mWaveAnimations.add(Tweener.to(mPointCloud.waveManager, WAVE_ANIMATION_DURATION, "ease", Ease.Quad.easeOut, "delay", 0, "radius", 2.0f * mOuterRadius,
				"onUpdate", mUpdateListener, "onComplete", new AnimatorListenerAdapter() {
					public void onAnimationEnd(Animator animator) {
						mPointCloud.waveManager.setRadius(0.0f);
						mPointCloud.waveManager.setAlpha(0.0f);
					}
				}));
		mWaveAnimations.start();
	}

	/**
	 * Resets the widget to default state and cancels all animation. If animate
	 * is 'true', will animate objects into place. Otherwise, objects will snap
	 * back to place.
	 * 
	 * @param animate
	 */
	public void reset(boolean animate) {
		mGlowAnimations.stop();
		mTargetAnimations.stop();
		startBackgroundAnimation(0, 0.0f);
		stopAndHideWaveAnimation();
		hideTargets(animate, false);
		hideGlow(0, 0, 0.0f, null);
		Tweener.reset();
	}

	private void startBackgroundAnimation(int duration, float alpha) {
		final Drawable background = getBackground();
		if (mAlwaysTrackFinger && background != null) {
			if (mBackgroundAnimator != null) {
				mBackgroundAnimator.animator.cancel();
			}
			mBackgroundAnimator = Tweener.to(background, duration, "ease", Ease.Cubic.easeIn, "alpha", (int) (255.0f * alpha), "delay", SHOW_ANIMATION_DELAY);
			mBackgroundAnimator.animator.start();
		}
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		final int action = event.getActionMasked();
		boolean handled = false;
		switch (action) {
		case MotionEvent.ACTION_POINTER_DOWN:
		case MotionEvent.ACTION_DOWN:
			if (DEBUG)
				Log.v(TAG, "*** DOWN ***");
			handleDown(event);
			handleMove(event);
			handled = true;
			break;

		case MotionEvent.ACTION_MOVE:
			if (DEBUG)
				Log.v(TAG, "*** MOVE ***");
			handleMove(event);
			handled = true;
			break;

		case MotionEvent.ACTION_POINTER_UP:
		case MotionEvent.ACTION_UP:
			if (DEBUG)
				Log.v(TAG, "*** UP ***");
			handleMove(event);
			handleUp(event);
			handled = true;
			break;

		case MotionEvent.ACTION_CANCEL:
			if (DEBUG)
				Log.v(TAG, "*** CANCEL ***");
			handleMove(event);
			handleCancel(event);
			handled = true;
			break;
		}
		invalidate();
		return handled ? true : super.onTouchEvent(event);
	}

	private void updateGlowPosition(float x, float y) {
		mPointCloud.glowManager.setX(x);
		mPointCloud.glowManager.setY(y);
	}

	private void handleDown(MotionEvent event) {
		int actionIndex = event.getActionIndex();
		float eventX = event.getX(actionIndex);
		float eventY = event.getY(actionIndex);
		switchToState(STATE_START, eventX, eventY);
		if (!trySwitchToFirstTouchState(eventX, eventY)) {
			mDragging = false;
		} else {
			mPointerId = event.getPointerId(actionIndex);
			updateGlowPosition(eventX, eventY);
		}
	}

	private void handleUp(MotionEvent event) {
		if (DEBUG && mDragging)
			Log.v(TAG, "** Handle RELEASE");
		int actionIndex = event.getActionIndex();
		if (event.getPointerId(actionIndex) == mPointerId) {
			switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex));
		}
	}

	private void handleCancel(MotionEvent event) {
		if (DEBUG && mDragging)
			Log.v(TAG, "** Handle CANCEL");

		// We should drop the active target here but it interferes with
		// moving off the screen in the direction of the navigation bar. At some
		// point we may
		// want to revisit how we handle this. For now we'll allow a canceled
		// event to
		// activate the current target.

		// mActiveTarget = -1; // Drop the active target if canceled.

		int actionIndex = event.findPointerIndex(mPointerId);
		actionIndex = actionIndex == -1 ? 0 : actionIndex;
		switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex));
	}

	private void handleMove(MotionEvent event) {
		int activeTarget = -1;
		final int historySize = event.getHistorySize();
		ArrayList<TargetDrawable> targets = mTargetDrawables;
		int ntargets = targets.size();
		float x = 0.0f;
		float y = 0.0f;
		int actionIndex = event.findPointerIndex(mPointerId);

		if (actionIndex == -1) {
			return; // no data for this pointer
		}

		for (int k = 0; k < historySize + 1; k++) {
			float eventX = k < historySize ? event.getHistoricalX(actionIndex, k) : event.getX(actionIndex);
			float eventY = k < historySize ? event.getHistoricalY(actionIndex, k) : event.getY(actionIndex);
			// tx and ty are relative to wave center
			float tx = eventX - mWaveCenterX;
			float ty = eventY - mWaveCenterY;
			float touchRadius = (float) Math.sqrt(dist2(tx, ty));
			final float scale = touchRadius > mOuterRadius ? mOuterRadius / touchRadius : 1.0f;
			float limitX = tx * scale;
			float limitY = ty * scale;
			double angleRad = Math.atan2(-ty, tx);

			if (!mDragging) {
				trySwitchToFirstTouchState(eventX, eventY);
			}

			if (mDragging) {
				// For multiple targets, snap to the one that matches
				final float snapRadius = mOuterRadius - mSnapMargin;
				final float snapDistance2 = snapRadius * snapRadius;
				// Find first target in range
				for (int i = 0; i < ntargets; i++) {
					TargetDrawable target = targets.get(i);

					double targetMinRad = (i - 0.5) * 2 * Math.PI / ntargets;
					double targetMaxRad = (i + 0.5) * 2 * Math.PI / ntargets;
					if (target.isEnabled()) {
						boolean angleMatches = (angleRad > targetMinRad && angleRad <= targetMaxRad)
								|| (angleRad + 2 * Math.PI > targetMinRad && angleRad + 2 * Math.PI <= targetMaxRad);
						if (angleMatches && (dist2(tx, ty) > snapDistance2)) {
							activeTarget = i;
						}
					}
				}
			}
			x = limitX;
			y = limitY;
		}

		if (!mDragging) {
			return;
		}

		if (activeTarget != -1) {
			switchToState(STATE_SNAP, x, y);
			updateGlowPosition(x, y);
		} else {
			switchToState(STATE_TRACKING, x, y);
			updateGlowPosition(x, y);
		}

		if (mActiveTarget != activeTarget) {
			// Defocus the old target
			if (mActiveTarget != -1) {
				TargetDrawable target = targets.get(mActiveTarget);
				target.setState(TargetDrawable.STATE_INACTIVE);
			}
			// Focus the new target
			if (activeTarget != -1) {
				TargetDrawable target = targets.get(activeTarget);
				target.setState(TargetDrawable.STATE_FOCUSED);
				final AccessibilityManager accessibilityManager = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
				// if (accessibilityManager.isEnabled()) {
				// String targetContentDescription =
				// getTargetDescription(activeTarget);
				// announceForAccessibility(targetContentDescription);
				// }
			}
		}
		mActiveTarget = activeTarget;
	}

	// @Override
	// public boolean onHoverEvent(MotionEvent event) {
	// final AccessibilityManager accessibilityManager =
	// (AccessibilityManager) getContext().getSystemService(
	// Context.ACCESSIBILITY_SERVICE);
	// if (accessibilityManager.isTouchExplorationEnabled()) {
	// final int action = event.getAction();
	// switch (action) {
	// case MotionEvent.ACTION_HOVER_ENTER:
	// event.setAction(MotionEvent.ACTION_DOWN);
	// break;
	// case MotionEvent.ACTION_HOVER_MOVE:
	// event.setAction(MotionEvent.ACTION_MOVE);
	// break;
	// case MotionEvent.ACTION_HOVER_EXIT:
	// event.setAction(MotionEvent.ACTION_UP);
	// break;
	// }
	// onTouchEvent(event);
	// event.setAction(action);
	// }
	// super.onHoverEvent(event);
	// return true;
	// }

	/**
	 * Sets the current grabbed state, and dispatches a grabbed state change
	 * event to our listener.
	 */
	private void setGrabbedState(int newState) {
		if (newState != mGrabbedState) {
			if (newState != OnTriggerListener.NO_HANDLE) {
				vibrate();
			}
			mGrabbedState = newState;
			if (mOnTriggerListener != null) {
				if (newState == OnTriggerListener.NO_HANDLE) {
					mOnTriggerListener.onReleased(this, OnTriggerListener.CENTER_HANDLE);
				} else {
					mOnTriggerListener.onGrabbed(this, OnTriggerListener.CENTER_HANDLE);
				}
				mOnTriggerListener.onGrabbedStateChange(this, newState);
			}
		}
	}

	private boolean trySwitchToFirstTouchState(float x, float y) {
		final float tx = x - mWaveCenterX;
		final float ty = y - mWaveCenterY;
		if (mAlwaysTrackFinger || dist2(tx, ty) <= getScaledGlowRadiusSquared()) {
			if (DEBUG)
				Log.v(TAG, "** Handle HIT");
			switchToState(STATE_FIRST_TOUCH, x, y);
			updateGlowPosition(tx, ty);
			mDragging = true;
			return true;
		}
		return false;
	}

	private void assignDefaultsIfNeeded() {
		if (mOuterRadius == 0.0f) {
			mOuterRadius = Math.max(mOuterRing.getWidth(), mOuterRing.getHeight()) / 2.0f;
		}
		if (mSnapMargin == 0.0f) {
			mSnapMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, SNAP_MARGIN_DEFAULT, getContext().getResources().getDisplayMetrics());
		}
		if (mInnerRadius == 0.0f) {
			mInnerRadius = mHandleDrawable.getWidth() / 10.0f;
		}
	}

	@TargetApi(17)
	private void computeInsets(int dx, int dy) {
		final int layoutDirection = getLayoutDirection();
		final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);

		switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
		case Gravity.LEFT:
			mHorizontalInset = 0;
			break;
		case Gravity.RIGHT:
			mHorizontalInset = dx;
			break;
		case Gravity.CENTER_HORIZONTAL:
		default:
			mHorizontalInset = dx / 2;
			break;
		}
		switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) {
		case Gravity.TOP:
			mVerticalInset = 0;
			break;
		case Gravity.BOTTOM:
			mVerticalInset = dy;
			break;
		case Gravity.CENTER_VERTICAL:
		default:
			mVerticalInset = dy / 2;
			break;
		}
	}

	@Override
	protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
		super.onLayout(changed, left, top, right, bottom);
		final int width = right - left;
		final int height = bottom - top;

		// Target placement width/height. This puts the targets on the greater
		// of the ring
		// width or the specified outer radius.
		final float placementWidth = Math.max(mOuterRing.getWidth(), 2 * mOuterRadius);
		final float placementHeight = Math.max(mOuterRing.getHeight(), 2 * mOuterRadius);
		float newWaveCenterX = mHorizontalInset + Math.max(width, mMaxTargetWidth + placementWidth) / 2;
		float newWaveCenterY = mVerticalInset + Math.max(height, +mMaxTargetHeight + placementHeight) / 2;

		if (mInitialLayout) {
			stopAndHideWaveAnimation();
			if (mShowTargetsOnIdle)
				showTargets(false);
			else
				hideTargets(false, false);
			mInitialLayout = false;
		}

		mOuterRing.setPositionX(newWaveCenterX);
		mOuterRing.setPositionY(newWaveCenterY);

		mHandleDrawable.setPositionX(newWaveCenterX);
		mHandleDrawable.setPositionY(newWaveCenterY);

		updateTargetPositions(newWaveCenterX, newWaveCenterY);
		updatePointCloudPosition(newWaveCenterX, newWaveCenterY);
		updateGlowPosition(newWaveCenterX, newWaveCenterY);

		mWaveCenterX = newWaveCenterX;
		mWaveCenterY = newWaveCenterY;

		if (DEBUG)
			dump();
	}

	private void updateTargetPositions(float centerX, float centerY) {
		// Reposition the target drawables if the view changed.
		ArrayList<TargetDrawable> targets = mTargetDrawables;
		final int size = targets.size();
		final float alpha = (float) (-2.0f * Math.PI / size);
		for (int i = 0; i < size; i++) {
			final TargetDrawable targetIcon = targets.get(i);
			final float angle = alpha * i;
			targetIcon.setPositionX(centerX);
			targetIcon.setPositionY(centerY);
			targetIcon.setX(mOuterRadius * (float) Math.cos(angle));
			targetIcon.setY(mOuterRadius * (float) Math.sin(angle));
		}
	}

	private void updatePointCloudPosition(float centerX, float centerY) {
		mPointCloud.setCenter(centerX, centerY);
	}

	@Override
	protected void onDraw(Canvas canvas) {
		mPointCloud.draw(canvas);
		mOuterRing.draw(canvas);
		final int ntargets = mTargetDrawables.size();
		for (int i = 0; i < ntargets; i++) {
			TargetDrawable target = mTargetDrawables.get(i);
			if (target != null) {
				target.draw(canvas);
			}
		}
		mHandleDrawable.draw(canvas);
	}

	public void setOnTriggerListener(OnTriggerListener listener) {
		mOnTriggerListener = listener;
	}

	private float square(float d) {
		return d * d;
	}

	private float dist2(float dx, float dy) {
		return dx * dx + dy * dy;
	}

	private float getScaledGlowRadiusSquared() {
		final float scaledTapRadius;
		final AccessibilityManager accessibilityManager = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
		if (accessibilityManager.isEnabled()) {
			scaledTapRadius = TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mGlowRadius;
		} else {
			scaledTapRadius = mGlowRadius;
		}
		return square(scaledTapRadius);
	}

	@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
	private void announceTargets() {
		StringBuilder utterance = new StringBuilder();
		final int targetCount = mTargetDrawables.size();
		for (int i = 0; i < targetCount; i++) {
			String targetDescription = getTargetDescription(i);
			String directionDescription = getDirectionDescription(i);
			if (!TextUtils.isEmpty(targetDescription) && !TextUtils.isEmpty(directionDescription)) {
				String text = String.format(directionDescription, targetDescription);
				utterance.append(text);
			}
		}
		if (utterance.length() > 0) {
			if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN)
				announceForAccessibility(utterance.toString());
		}
	}

	private String getTargetDescription(int index) {
		if (mTargetDescriptions == null || mTargetDescriptions.isEmpty()) {
			mTargetDescriptions = loadDescriptions(mTargetDescriptionsResourceId);
			if (mTargetDrawables.size() != mTargetDescriptions.size()) {
				Log.w(TAG, "The number of target drawables must be" + " equal to the number of target descriptions.");
				return null;
			}
		}
		return mTargetDescriptions.get(index);
	}

	private String getDirectionDescription(int index) {
		if (mDirectionDescriptions == null || mDirectionDescriptions.isEmpty()) {
			mDirectionDescriptions = loadDescriptions(mDirectionDescriptionsResourceId);
			if (mTargetDrawables.size() != mDirectionDescriptions.size()) {
				Log.w(TAG, "The number of target drawables must be" + " equal to the number of direction descriptions.");
				return null;
			}
		}
		return mDirectionDescriptions.get(index);
	}

	private ArrayList<String> loadDescriptions(int resourceId) {
		TypedArray array = getContext().getResources().obtainTypedArray(resourceId);
		final int count = array.length();
		ArrayList<String> targetContentDescriptions = new ArrayList<String>(count);
		for (int i = 0; i < count; i++) {
			String contentDescription = array.getString(i);
			targetContentDescriptions.add(contentDescription);
		}
		array.recycle();
		return targetContentDescriptions;
	}

	public int getResourceIdForTarget(int index) {
		final TargetDrawable drawable = mTargetDrawables.get(index);
		return drawable == null ? 0 : drawable.getResourceId();
	}

	public void setEnableTarget(int resourceId, boolean enabled) {
		for (int i = 0; i < mTargetDrawables.size(); i++) {
			final TargetDrawable target = mTargetDrawables.get(i);
			if (target.getResourceId() == resourceId) {
				target.setEnabled(enabled);
				break; // should never be more than one match
			}
		}
	}

	/**
	 * Gets the position of a target in the array that matches the given
	 * resource.
	 * 
	 * @param resourceId
	 * @return the index or -1 if not found
	 */
	public int getTargetPosition(int resourceId) {
		for (int i = 0; i < mTargetDrawables.size(); i++) {
			final TargetDrawable target = mTargetDrawables.get(i);
			if (target.getResourceId() == resourceId) {
				return i; // should never be more than one match
			}
		}
		return -1;
	}

	private boolean replaceTargetDrawables(Resources res, int existingResourceId, int newResourceId) {
		if (existingResourceId == 0 || newResourceId == 0) {
			return false;
		}

		boolean result = false;
		final ArrayList<TargetDrawable> drawables = mTargetDrawables;
		final int size = drawables.size();
		for (int i = 0; i < size; i++) {
			final TargetDrawable target = drawables.get(i);
			if (target != null && target.getResourceId() == existingResourceId) {
				target.setDrawable(res, newResourceId);
				result = true;
			}
		}

		if (result) {
			requestLayout(); // in case any given drawable's size changes
		}

		return result;
	}

	/**
	 * Searches the given package for a resource to use to replace the Drawable
	 * on the target with the given resource id
	 * 
	 * @param component
	 *            of the .apk that contains the resource
	 * @param name
	 *            of the metadata in the .apk
	 * @param existingResId
	 *            the resource id of the target to search for
	 * @return true if found in the given package and replaced at least one
	 *         target Drawables
	 */
	public boolean replaceTargetDrawablesIfPresent(ComponentName component, String name, int existingResId) {
		if (existingResId == 0)
			return false;

		boolean replaced = false;
		if (component != null) {
			try {
				PackageManager packageManager = getContext().getPackageManager();
				// Look for the search icon specified in the activity meta-data
				Bundle metaData = packageManager.getActivityInfo(component, PackageManager.GET_META_DATA).metaData;
				if (metaData != null) {
					int iconResId = metaData.getInt(name);
					if (iconResId != 0) {
						Resources res = packageManager.getResourcesForActivity(component);
						replaced = replaceTargetDrawables(res, existingResId, iconResId);
					}
				}
			} catch (NameNotFoundException e) {
				Log.w(TAG, "Failed to swap drawable; " + component.flattenToShortString() + " not found", e);
			} catch (Resources.NotFoundException nfe) {
				Log.w(TAG, "Failed to swap drawable from " + component.flattenToShortString(), nfe);
			}
		}
		if (!replaced) {
			// Restore the original drawable
			replaceTargetDrawables(getContext().getResources(), existingResId, existingResId);
		}
		return replaced;
	}

	public boolean isShowTargetsOnIdle() {
		return mShowTargetsOnIdle;
	}

	public void setShowTargetsOnIdle(boolean mShowTargetsOnIdle) {
		this.mShowTargetsOnIdle = mShowTargetsOnIdle;
		
	}
}